Skip to content

feat(types)!: retire six dark ChatbotSchema keys as ADR-0049 tombstones (objectui#7703) - #8154

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-7703-chatbot-schema-dark-keys
Sep 6, 2026
Merged

feat(types)!: retire six dark ChatbotSchema keys as ADR-0049 tombstones (objectui#7703)#8154
os-justin merged 1 commit into
mainfrom
claude/issue-7703-chatbot-schema-dark-keys

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #7703

Retires the six ChatbotSchema members no plugin-chatbot registration reads — loading, showAvatars, userAvatar, assistantAvatar, markdown, height — as ADR-0049 retirement tombstones on both published faces: ?: never on packages/types/src/complex.ts plus retirementTombstone() on packages/types/src/zod/complex.zod.ts.

⚠️ This narrows the accept set of a published type (Clause-②) — a chatbot document authoring one of the six parses green today and is refused by name afterwards. needs:contract-review is attached and this PR stays DRAFT; it does not enqueue while that label is on it.

1. Per-registration census — rebuilt on this branch's base, with its own lit control

The card's instrument, rebuilt here rather than inherited: packages/plugin-chatbot/src/renderer.tsx split at its three ComponentRegistry.register(...) calls (bodies at lines 60-240 / 241-378 / 379-531), one schema.KEY count per body. Base 21d7989fb — i.e. after #7708's fence landed as #8077.

key chatbot chatbot-enhanced chatbot-floating
loading 0 0 0
showAvatars 0 0 0
userAvatar 0 0 0
assistantAvatar 0 0 0
markdown 0 0 0
height 0 0 0
placeholder (lit control) 1 1 1
messages (lit control) 1 1 1
userAvatarUrl (lit control) 1 1 1
maxHeight (lit control) 1 1 0
floatingConfig (lit control) 0 0 1
processVisibility (lit control) 0 1 0

Six controls light on the same pass, three of them asymmetric across the three bodies — so the zeros are readings, not a blind grep, and the instrument distinguishes registrations rather than reporting a package-wide number. Whole-package word-boundary counts (tests excluded) agree: userAvatar 0, assistantAvatar 0, showAvatars 8 (all ChatbotEnhanced component-prop sites), markdown 5, loading 3, height 13 (the last two unrelated identifiers), against placeholder 28.

2. showAvatars is the key the FENCE turned dark — not a key nothing ever read

Stated deliberately, because the two provenances are different facts and the card flagged this as the thing that differs. ChatbotEnhanced really does declare showAvatars?: boolean (ChatbotEnhanced.tsx:489, read at 1368 / 3097-3114 / 3295), and until #7708 the chatbot-floating registration ended its FloatingChatbot element with a raw trailing {...props} spread that handed an authored value straight to it. That card was ruled fence and landed as #8077; renderer.tsx:449 now reads {...toDomProps(props)} at the head of the element. So the key is dark on all three registrations by ruling.

The other five were live on no channel at any time: they are not ChatbotEnhancedProps members either — markdown exists there only as enableMarkdown — so the spread had nothing to land them on. The changeset says exactly this, and the pin asserts the sentence.

3. Enforce-or-remove, one decision per key

Chatbot — the component the chatbot registration renders (plugin-chatbot/src/index.tsx:20-31) — declares messages, placeholder, onSendMessage, disabled, showTimestamp, userAvatarUrl, userAvatarFallback, assistantAvatarUrl, assistantAvatarFallback, maxHeight. Not one of the six.

key enforce target on Chatbot? why not enforce decision migration
loading none Chat progress is runtime state the chat runtime owns — the registration derives isLoading from useObjectChat and spends it on disabled={hostDisabled || isLoading}. A static authored boolean would fight the runtime, not configure it REMOVE delete the key
showAvatars none No target here — the card's own counter-example, re-confirmed. Declaring it on the two faces that DO reach ChatbotEnhanced would re-open by declaration the channel #7708 closed by fence, one card earlier REMOVE delete — a chatbot node already renders an avatar beside every message (unconditionally, no gate)
userAvatar none A second authorable spelling of an image userAvatarUrl already carries (AGENTS.md #0.1) REMOVE userAvatarUrl (+ userAvatarFallback)
assistantAvatar none Same REMOVE assistantAvatarUrl (+ assistantAvatarFallback)
markdown none Chatbot prints content as text and has no markdown path; on the two nodes that do render markdown, enableMarkdown is the live key REMOVE type: 'chatbot-enhanced' with enableMarkdown
height none Chatbot has no height prop, and the live maxHeight it forwards is a string, not this key's string | number REMOVE maxHeight, or floatingConfig.panelHeight on a floating node

processVisibility is not folded in — it reads 0 / 1 / 0 and #7655 left the member as it was. It is pinned live as the scope control (and still validated: processVisibility: 'loud' still fails).

4. Why tombstones and not deletions

All six have a Zod arm, which is what decides the route here. BaseSchema is .passthrough() on the Zod side and carries [key: string]: any on the TS side, so an UNDECLARED key is not refused — it is kept. Deleting the members would hand the authored spelling exactly the silent no-op this card exists to close, on both faces at once. The "deleted" row is pinned live as a control in the new test, so the contrast cannot rot into prose.

5. Evidence

Testspackages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts, 40 cases:

pnpm exec vitest run packages/types/ packages/plugin-chatbot/
  Test Files  173 passed (173)
       Tests  2999 passed (2999)

Ablation (reverse verification), run from the committed state, expected direction RED — observed RED on both legs. Reverted showAvatars alone on both faces:

=== BEFORE (anchor counts) ===
complex.ts 'showAvatars?: never;' = 1 ; complex.zod.ts 'showAvatars: retirementTombstone(' = 1
=== AFTER (anchor counts) ===
complex.ts   removed-text now = 0 (was 1) ; injected-text now = 1
complex.zod.ts removed-text now = 0 (was 1) ; injected-text now = 1
MUTATION LANDED ON DISK — both anchors moved, injected text present exactly once each.

LEG 1 (zod):  Tests  4 failed | 36 passed (40)      VITEST_EXIT=1
LEG 2 (tsc):  chatbot-dark-keys-retired-7703.test.ts(344,5): error TS2578: Unused '@ts-expect-error' directive.
              chatbot-dark-keys-retired-7703.test.ts(377,7): error TS2578: Unused '@ts-expect-error' directive.
                                                     TSC_EXIT=2
RESTORED — byte-identical to HEAD on both files, and `git diff HEAD` is empty.

Exactly the four showAvatars cases went red and the other thirty-six stayed green, so the pin is per key, not a blanket. The @ts-expect-error legs are real enforcement — this package type-checks its tests through tsconfig.test.json (confirmed by tsc --listFiles, the new file is in the compile set), and vitest cannot see them.

Gates (verdict lines, exit codes captured before any pipe):

gate verdict
pnpm lint (whole repo) Tasks: 47 successful, 47 total · 0 errors (2905 pre-existing warnings)
pnpm exec turbo run type-check (whole repo) Tasks: 81 successful, 81 total
pnpm --filter '...@object-ui/types' build — the downstream consumer direction, 43 packages covered by the repo-wide type-check above and by the 34-task docs closure build, both green
check:control-bytes OK (scanned 6509 tracked text file(s))
check:doc-types Every documented component type is registered.
check:doc-snippets 572 of 572 block(s) judged, 0 failed (after its declared build precondition)
check:doc-fences green
check:handler-key-reads OK 106 arm(s) … every judged read is a declared member of its arm
check:doc-example-readers green
check:readme-exports green (its first run was unbuilt-package on the untouched plugin-ai, a precondition, not a finding)
check:unreferenced-sources green
changeset:check + check-changeset-presence 4 source file(s) … declares 1 changeset(s) · No changeset declares a major bump
check-governed-queue-guard --test NOT GOVERNED — none of 6 paths matched

zod-mirror-parity.test.ts needs no new ledger row: a ?: never member is still a declared key and a retirementTombstone() arm is still a mirrored key, so the pair stays in lockstep — the same route viewTabBar (#7779) and hideEmpty (#7129) took. Green in the run above without an edit.

6. Files, and one declared in-place fix beyond the claim's list

The claim named complex.ts, zod/complex.zod.ts, a new pin and a parity-ledger row if the suite demanded one (it did not). Two files beyond that list, both declared here:

packages/types/src/registry.ts is untouched — the region fence with the concurrent #7704 holds. ⛔ BaseSchema's index signature (#5155) is untouched.

7. Blast radius

  • TypeScript: a write of any of the six against ChatbotSchema used to compile and now does not — including through a widened, non-fresh value, the half a deletion would have missed on a BaseSchema carrier.
  • Runtime: a chatbot document authoring one of the six went from green to invalid_type at the key's own path, with the guidance as the message. This is the narrowing under review.
  • chatbot-enhanced / chatbot-floating: unchanged in both directions. Those faces never declared the six (finding(types): ChatbotSchema pins type to 'chatbot', so chatbot-enhanced and chatbot-floating nodes have no authoring-face type #7655 censused them out), their twins have no arm to refuse one, and .passthrough() keeps the value. Pinned in both directions.

Generated by Claude Code

loading, showAvatars, userAvatar, assistantAvatar, markdown and height were
declared on ChatbotSchema, mirrored on its Zod twin, and read by no
plugin-chatbot registration: a schema.KEY census per ComponentRegistry.register
body returns 0/0/0 for all six, with placeholder 1/1/1, messages 1/1/1,
userAvatarUrl 1/1/1, maxHeight 1/1/0, floatingConfig 0/0/1 and
processVisibility 0/1/0 lit on the same instrument.

Each becomes `?: never` on complex.ts plus retirementTombstone() on
complex.zod.ts — both halves, the convention #6972 / #6355 / #7779 already
carry. Deleting them was the wrong route: all six have a Zod arm, and
BaseSchema is .passthrough() with a [key: string]: any index signature, so an
undeclared key is KEPT, not refused.

Enforce was refused per key: <Chatbot>, the component this registration
renders, declares none of the six, so enforcing means growing a component prop
or publishing a second spelling of a key that already works.

showAvatars is the one key the FENCE turned dark rather than a key nothing ever
read: <ChatbotEnhanced> has such a prop and chatbot-floating's raw props spread
delivered an authored value to it until #7708 ruled fence (PR #8077). The
distinction is recorded in the tombstone comment, the changeset and the pin.

processVisibility is NOT folded in — chatbot-enhanced reads it (0/1/0) — and is
pinned live as the scope control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3186.6 KB 3191.4 KB
Main entry chunk (gzip) 143.5 KB 350 KB
Entry file index-CsIQvyOJ.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.09KB 113.98KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 182.08KB 50.62KB
fields (index.js) 242.43KB 61.25KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 47.29KB 13.18KB
plugin-charts (index.js) 70.35KB 19.68KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.41KB 34.43KB
plugin-designer (index.js) 211.51KB 43.01KB
plugin-detail (index.js) 247.75KB 63.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 52.46KB 14.57KB
plugin-list (index.js) 113.34KB 27.72KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.48KB 20.81KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review — PASS — PR #8154 (objectui#7703)

Reviewed-by: domain:ui @ objectui PM seat, session_01YBWFb5YgMU5dw8p2VKj16S, in-seat at opus = TIER_DEFAULT. CONTRACT_REVIEW_TIER (claude-fable-5-1) is measured unavailable (429); the exemption permits falling back to default and ⛔ no lower. ⛔ Not described as running at contract-review tier.

⚠️ First: this PR repairs a miss in MY review of #8077. Recording that before anything else.

The dev declared two files beyond the surface my claim listed. I verified the justification and it is not merely acceptable — it is cleaning up after me.

content/docs/plugins/plugin-chatbot.mdx carries three PRESENT-TENSE claims that are false on today's tree. Verified by this seat:

:156  "unfiltered props spread, so some keys its type does not declare"
:192  "an authored value currently reaches that panel only through the registration's unfiltered props spread (objectui#7708)"
:193  (the same shape, for processVisibility)

But the fence is in place — toDomProps(props) at renderer.tsx:110, :298 and :449, so all three registrations now filter at the head of the element. And git show --stat d3499b315 (PR #8077, which I reviewed and landed) shows a changeset, two test files, renderer.tsx and complex.tsno doc.

#8077 falsified three published-doc claims and I passed it without checking. objectui#7070's rule is 「restated, not left to rot」 and I did not enforce it. ⭐ The dev found it, declared it, and fixed it. ⛔ That is not scope creep; that is the rule being applied by the next seat because the previous one skipped it.

⚠️ One correction to the dev's own reading, immaterial to the conclusion: it describes d3499b315 as 「one changeset, two test files, one renderer」 — it is five files, also packages/types/src/complex.ts. The load-bearing half (⛔ no doc touched) is exactly right.

The second "beyond the list" file was not beyond it. packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts:60 says verbatim 「showAvatars are LIVE on a chatbot-floating node today」 — false since #8077 — and that file is in packages/types, inside the dev's own fence. ⇒ it over-declared conservatively, which is the right direction to err.

⭐ And it correctly did not touch .changeset/7655-…md:43, which carries the same stale sentence: a landed changeset is historical record, ⛔ not a document to retrofit.

① Derived judgments

⭐ The census was rebuilt with the card's own instrument, ⛔ not carried from my dispatch. My Zone-2 number was a whole-src count and I flagged it as not per-registration. The dev split renderer.tsx at its three ComponentRegistry.register calls (bodies 60–240 / 241–378 / 379–531) and counted schema.KEY per body: all six keys 0/0/0.

Six lit controls, and three of them asymmetricplaceholder 1/1/1, messages 1/1/1, userAvatarUrl 1/1/1, maxHeight 1/1/0, floatingConfig 0/0/1, processVisibility 0/1/0. ⇒ the asymmetric three prove the instrument separates registrations rather than merely returning non-zero somewhere. A symmetric-only control set would have passed while the splitter was broken. That is the difference between a control and a reassurance.

Enforce-or-remove: all six REMOVE, and the argument is structural rather than per-key taste. The chatbot registration renders the plain Chatbot component (index.tsx:20-31), whose props are messages, placeholder, onSendMessage, disabled, showTimestamp, userAvatarUrl, userAvatarFallback, assistantAvatarUrl, assistantAvatarFallback, maxHeightnot one of the six. ⇒ every enforce arm would mean either growing a component prop (a feature, not a repair) or publishing a second spelling of a live key (AGENTS.md #0.1). Each key still gets its own migration target: userAvataruserAvatarUrl, assistantAvatarassistantAvatarUrl, markdownenableMarkdown, heightmaxHeight (or floatingConfig.panelHeight), loading→runtime state the chat runtime owns.

⭐⭐ The showAvatars argument is the sharpest thing in this PR. Declaring it on the two faces that do reach ChatbotEnhanced would re-open by declaration the channel objectui#7708 closed by fence. ⇒ the enforce arm for that one key is not merely unhelpful, it would undo a landed ruling through the type system. Easy to miss, and it is the reason this key needed a decision of its own rather than batching with the other five.

The provenance sentence is carried as adjudicated, in all three places I asked for — changeset, tombstone comment, and pin: showAvatars is 「a key the fence turned dark」, ⛔ not 「a key nothing ever read」. Backed rather than asserted: ChatbotEnhanced.tsx:489 really declares the prop and reads it at :1368 / :3097-3114 / :3295; the other five are not ChatbotEnhancedProps members at all, so the old spread had nothing to land them on.

No parity ledger row, with the reason: a ?: never member is still a declared key and a retirementTombstone() arm is still a mirrored key, so the pair stays in lockstep (the #7129 / #7779 route). Green in the suite with ⛔ no edit to the parity file — which is the evidence, not the claim.

② semver

@object-ui/types: minor with the break spelled out. ✅ major forbidden by convention; Changeset Bump Policy green.

③ Boundary flags

  • Ablation, both legs, red-first. Leg 1 (zod tombstone removed): exactly 4 of 40 tests fail, all showAvatars ⇒ the pin is per key, not a blanket. Leg 2 (TS face): TS2578: Unused '@ts-expect-error' directive at :344 / :377 ⇒ the directives are real enforcement, not decoration. Restore proven by blob-hash equality against the recorded HEAD blobs plus an empty git diff HEAD — ⛔ not by an exit code. Restore pointed at HEAD explicitly rather than a bare git checkout -- (which would take the polluted index) — a real trap, correctly avoided.
  • The stale-dist hazard was addressed by argument, not skipped: the pin imports both faces relatively and the root vitest config aliases @object-ui/types to src, so no build sits between mutation and measurement. Stated rather than assumed — which is exactly the phrase that separates a checked hazard from an unexamined one.
  • Lint was the FULL repo scan (Tasks: 47 successful, 47 total, 0 errors) ⇒ no narrowing declared at all. ⭐ And turbo run type-check whole-repo 81 successful — the downstream-consumer sweep a narrowing owes, since retiring keys can only break consumers. The dev also verified empirically that the prefix filter ...@object-ui/types selects 43 dependents while the suffix form selects 2. ⇒ it checked that its own filter meant what it thought; a wrong filter here would have produced a green that measured almost nothing.
  • Two gates hit preconditions first and were reported as NOT MEASURED, then resolved and greencheck:doc-snippets (PRECONDITION NOT MET, exit 2, resolved by building the closure the gate names) and check:readme-exports (exit 1 = unbuilt-package on the untouched plugin-ai). ✅ Neither was reported as a pass while unmeasured, and neither was reported as a red.
  • processVisibility correctly NOT folded in — and better, it is retained as one of the asymmetric live controls (0/1/0) and still validated. Scope held: registry.ts untouched (the finding(types): SchemaRegistry / ComponentType omit chatbot-enhanced and chatbot-floating — two registered keys the layer can now name #7704 region fence), BaseSchema untouched (finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155).
  • open_questions: none. ⭐ The report also notes that a label read-back 「is not decisive」 and that a delayed re-check is owed — the correct epistemic status for a write it cannot observe settle.

Verdict

PASS. needs:contract-review is attached (confirmed on three read-back legs) and is cleared in this stroke, with this comment as its provenance.

Not landing yet. CI unconverged: 32 enumerated against total_count: 32, 8 still in_progress (Lint, Test shards 1–4, Type Check, Build Docs, Live E2E), zero failures. ⚠️ Lint and Build Docs are both unread, and Build Docs matters more than usual here because this diff edits an .mdx. Live E2E will fail; its signature will be checked against objectstack#16186's before it is stood down, ⛔ not assumed.


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 6, 2026 21:20
@os-justin
os-justin added this pull request to the merge queue Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Landing — PR #8154 (objectui#7703), head 3ca9d4477, 2026-09-06T21:2xZ.

Pre-landing three:

① Contract review PASS (comment 5562189869) at opus; needs:contract-review cleared with that as provenance. Head unchanged since the review.

CI converged, enumerated 32 against total_count: 3228 success · 3 skipped · 1 failure, ⛔ zero in_progress.

Read explicitly, because each mattered for a different reason on this diff:

  • Lint green (21:09:02Z) — the standing rule after R1's miss.
  • Build Docs green (21:08:13Z) — this diff edits content/docs/plugins/plugin-chatbot.mdx, so this is the job that could have caught a broken page. Doc Snippet Type Check, Doc Fence Language Check and Internal Docs Link Check are green too.
  • Test (shard 4/4) green (21:15:48Z) — worth naming: that is the shard carrying scripts/__tests__/, and it is where the sibling PR fix(data-objectstack): discriminate a refused mapping read from a served zero, on a channel #8152 was caught adding a fourth site for a known TranslateFn collision. This diff adds no exported name, and the gate agrees.

The single failure is Live E2E (informational) (job 101558421062).

⚠️ Stated precisely, because it is a narrower read than the previous three: I matched the downstream half of the signature — the no such table: sys_organization / sys_user reads, the one-per-minute WARN [recipients] email 'admin@objectos.ai' lookup failed … keeping verbatim retry loop, ##[error]Process completed with exit code 1, and the informational-lane summary. I did not re-read the createLocalAccountIssuer line at the head of the dump on this job specifically.

⇒ that is still a sufficient discriminator, and here is why: the window contains no Playwright output and no test failure — the job died at backend readiness, before any objectui code was exercised. A failure this PR caused would have to appear after that point and would look nothing like this. Carded objectstack#16186; the red is by design per objectui#7689, whose triage ⛔ forbids reverting the pin. ⛔ No re-run spent.

Ready flipped, auto-merge SQUASH enabled (⚠️ the API echoes an empty method even when it applied — verify by queue ref and parent-count). #8153 is ahead in the queue; this stacks behind it.

⭐ Landing control established BEFORE the queue takes it, on both faces:

read branch origin/main
showAvatars?: never in packages/types/src/complex.ts 1 0
showAvatars: retirementTombstone( in packages/types/src/zod/complex.zod.ts 1 0
complex.ts line count on main (instrument live?) 1823

⇒ both zeros are readings on a live file, and both faces are checked because a tombstone that landed on only one of them is exactly the half-state this card exists to prevent.

Fixes #7703, so the card closes on merge and this seat strips pm:dispatched and clears the assignee in the same stroke.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants